master @ 248 LINES
[ HISTORY ] [ UP ]
┌─ ASTRO ────────────────────────────────────────────────────────────────────┐
│ --- │
│ // Unified Players leaderboard page. Handles 4 combinations via URL/query: │
│ // - Season: season1, season2, all-time │
│ // - Sort: ?sort=time (default) | ?sort=runs │
│ // - View: /players/... (this page) │
│ // The /characters/... route is the Character view counterpart and uses │
│ // almost the same logic (only the view + class scope differ). │
│ import PageLayout from "../../../../layouts/PageLayout.astro"; │
│ import LeaderboardTable from "../../../../components/Leaderboard/LeaderboardTable/ │
│ LeaderboardTable.astro"; │
│ import Pagination from "../../../../components/Leaderboard/Pagination/Pagination.a │
│ stro"; │
│ import LeaderboardScopeFilter from "../../../../components/Leaderboard/Leaderboard │
│ ScopeFilter/LeaderboardScopeFilter.astro"; │
│ import LeaderboardTypeNav from "../../../../components/Leaderboard/LeaderboardType │
│ Nav/LeaderboardTypeNav.astro"; │
│ import LeaderboardHeader from "../../../../components/Leaderboard/LeaderboardHeade │
│ r/LeaderboardHeader.astro"; │
│ import PlayerSearch from "../../../../components/PlayerSearch/PlayerSearch.astro"; │
│ import { fetchUnifiedLeaderboard } from "../../../../lib/api"; │
│ import { getEffectiveRealmSlug } from "../../../../lib/realms"; │
│ │
│ export const prerender = false; │
│ │
│ const seasonParam = Astro.params.season || "season3"; │
│ const seasonMatch = seasonParam.match(/^season(\d+)$/); │
│ const isAllTime = seasonParam === "all-time"; │
│ const currentSeason: number | "all-time" = isAllTime │
│ ? "all-time" │
│ : seasonMatch │
│ ? parseInt(seasonMatch[1], 10) │
│ : 3; │
│ │
│ const scope = Astro.params.scope || ""; │
│ let scopeParts = scope.split("/").filter(Boolean); │
│ │
│ // Sort lives at the front of the scope catchall as `by-runs` (default │
│ // `by-time` is omitted). │
│ let sortParam: "time" | "runs" = "time"; │
│ if (scopeParts[0] === "by-runs") { │
│ sortParam = "runs"; │
│ scopeParts = scopeParts.slice(1); │
│ } │
│ │
│ // Old shape was /players/{sort}/... - strip and 301 to canonical. │
│ if ( │
│ scopeParts.length > 0 && │
│ (scopeParts[0] === "overall-time" || scopeParts[0] === "total-runs") │
│ ) { │
│ const tail = scopeParts.slice(1).join("/"); │
│ return Astro.redirect( │
│ `/challenge-mode/${seasonParam}/players/${tail || "global"}`, │
│ 301, │
│ ); │
│ } │
│ │
│ let regionParam = "global"; │
│ let realmParam = ""; │
│ let apiScope: "global" | "regional" | "realm" = "global"; │
│ if (scopeParts.length >= 1) { │
│ regionParam = scopeParts[0]; │
│ if (regionParam !== "global") { │
│ apiScope = "regional"; │
│ if (scopeParts.length >= 2 && scopeParts[1] && scopeParts[1] !== "all") { │
│ realmParam = scopeParts[1]; │
│ apiScope = "realm"; │
│ } │
│ } │
│ } │
│ │
│ // Connected-realm rollup: only parent realms have JSON files. If the user │
│ // landed on a child realm slug (e.g. eu/garalon ? child of mirage-raceway), │
│ // 301 to the parent so the URL matches the data we serve. │
│ if (apiScope === "realm") { │
│ const parent = getEffectiveRealmSlug(regionParam, realmParam); │
│ if (parent && parent !== realmParam) { │
│ const tail = scopeParts.slice(2).join("/"); │
│ const newPath = │
│ `/challenge-mode/${seasonParam}/players/${regionParam}/${parent}` + │
│ (tail ? `/${tail}` : ""); │
│ const qs = Astro.url.searchParams.toString(); │
│ return Astro.redirect(qs ? `${newPath}?${qs}` : newPath, 301); │
│ } │
│ } │
│ │
│ const pageParam = Astro.url.searchParams.get("page"); │
│ const currentPage = pageParam ? parseInt(pageParam, 10) : 1; │
│ │
│ const seasonForNav: number = isAllTime ? 2 : (currentSeason as number); │
│ │
│ let scopeLabel: string; │
│ if (regionParam === "global") { │
│ scopeLabel = "Global"; │
│ } else if (realmParam) { │
│ scopeLabel = `${regionParam.toUpperCase()} - ${realmParam}`; │
│ } else { │
│ scopeLabel = regionParam.toUpperCase(); │
│ } │
│ const seasonLabel = isAllTime ? "All Time" : `Season ${currentSeason}`; │
│ │
│ let data; │
│ try { │
│ data = await fetchUnifiedLeaderboard( │
│ "player", │
│ sortParam, │
│ currentSeason, │
│ apiScope, │
│ { │
│ region: regionParam, │
│ realmSlug: realmParam || undefined, │
│ page: currentPage, │
│ }, │
│ Astro.url.origin, │
│ ); │
│ } catch (error) { │
│ console.error("[Players page] Error:", error); │
│ throw error; │
│ } │
│ │
│ const pageTitle = `Player Rankings - ${scopeLabel} - ${seasonLabel}`; │
│ const dungeonName = │
│ sortParam === "runs" ? "Player Rankings - Total Runs" : "Player Rankings"; │
│ │
│ // Build toggle URLs - sort lives in the path now (`by-runs` segment between │
│ // view and scope), so toggling rebuilds the path. Other query params (e.g. │
│ // from future filters) are preserved minus `page`. │
│ const scopeTail = scopeParts.length ? scopeParts.join("/") : "global"; │
│ const otherParams = new URLSearchParams(Astro.url.searchParams); │
│ otherParams.delete("page"); │
│ otherParams.delete("sort"); │
│ const otherQS = otherParams.toString(); │
│ const qsSuffix = otherQS ? `?${otherQS}` : ""; │
│ │
│ function buildPath(view: "players" | "characters", sort: "time" | "runs") { │
│ const sortSeg = sort === "runs" ? "/by-runs" : ""; │
│ return `/challenge-mode/${seasonParam}/${view}${sortSeg}/${scopeTail}`; │
│ } │
│ const sortByTimeUrl = buildPath("players", "time") + qsSuffix; │
│ const sortByRunsUrl = buildPath("players", "runs") + qsSuffix; │
│ const viewPlayerUrl = buildPath("players", sortParam) + qsSuffix; │
│ const viewCharacterUrl = buildPath("characters", sortParam) + qsSuffix; │
│ │
│ // Determine which LeaderboardTable type to render. │
│ const tableType = sortParam === "runs" ? "account-total-runs" : "account"; │
│ --- │
│ │
│ <PageLayout title={pageTitle}> │
│ <main class="leaderboard-page"> │
│ <LeaderboardHeader │
│ season={seasonForNav} │
│ dungeonName={dungeonName} │
│ scopeLabel={`${seasonLabel} - ${scopeLabel}`} │
│ /> │
│ <PlayerSearch /> │
│ <LeaderboardTypeNav │
│ currentTab="player" │
│ currentRegion={regionParam} │
│ currentRealm={realmParam} │
│ currentSeason={seasonForNav} │
│ /> │
│ │
│ <div class="cm-filters"> │
│ <div class="filter-group"> │
│ <label for="view-filter">View</label> │
│ <select id="view-filter"> │
│ <option value="player" selected data-url={viewPlayerUrl} │
│ >Player</option │
│ > │
│ <option value="character" data-url={viewCharacterUrl} │
│ >Character</option │
│ > │
│ </select> │
│ </div> │
│ <div class="filter-group"> │
│ <label for="sort-filter">Sort</label> │
│ <select id="sort-filter"> │
│ <option │
│ value="time" │
│ selected={sortParam === "time"} │
│ data-url={sortByTimeUrl} │
│ > │
│ Time │
│ </option> │
│ <option │
│ value="runs" │
│ selected={sortParam === "runs"} │
│ data-url={sortByRunsUrl} │
│ > │
│ Total Runs │
│ </option> │
│ </select> │
│ </div> │
│ <LeaderboardScopeFilter │
│ currentRegion={regionParam} │
│ currentRealm={realmParam} │
│ currentSeason={isAllTime ? 0 : (currentSeason as number)} │
│ leaderboardType="player" │
│ currentSort={sortParam} │
│ /> │
│ </div> │
│ │
│ <div id="leaderboard-content"> │
│ <LeaderboardTable │
│ type={tableType} │
│ accounts={data.leaderboard} │
│ currentPage={data.pagination.currentPage} │
│ pageSize={data.pagination.pageSize} │
│ region={regionParam} │
│ realm={realmParam} │
│ /> │
│ </div> │
│ │
│ <Pagination │
│ currentPage={data.pagination.currentPage} │
│ totalPages={data.pagination.totalPages} │
│ hasNextPage={data.pagination.hasNextPage} │
│ hasPrevPage={data.pagination.hasPrevPage} │
│ totalPlayers={data.pagination.totalAccounts ?? │
│ data.pagination.totalPlayers ?? │
│ data.pagination.totalRuns ?? │
│ 0} │
│ baseUrl={Astro.url.pathname} │
│ /> │
│ </main> │
│ </PageLayout> │
│ │
│ <script> │
│ // Same data-url navigation pattern the other filters use. │
│ for (const id of ["view-filter", "sort-filter"]) { │
│ const el = document.getElementById(id) as HTMLSelectElement | null; │
│ if (!el) continue; │
│ el.addEventListener("change", (e) => { │
│ const target = e.target as HTMLSelectElement; │
│ const url = target.options[target.selectedIndex].dataset.url; │
│ if (url) window.location.href = url; │
│ }); │
│ } │
│ </script> │
│ │
│ <style> │
│ .leaderboard-page { │
│ max-width: 1200px; │
│ margin: 0 auto; │
│ padding: 20px; │
│ } │
│ │
│ @media (max-width: 768px) { │
│ .leaderboard-page { │
│ padding: 15px; │
│ } │
│ } │
│ </style> │
└────────────────────────────────────────────────────────────────────────────────────┘
┌─ ASTRO ──────────────────────────────┐
│ --- │
│ // Unified Players leaderboard page. Handles │
│ 4 combinations via URL/query: │
│ // - Season: season1, season2, all-time │
│ // - Sort: ?sort=time (default) | ?sort= │
│ runs │
│ // - View: /players/... (this page) │
│ // The /characters/... route is the Characte │
│ r view counterpart and uses │
│ // almost the same logic (only the view + cl │
│ ass scope differ). │
│ import PageLayout from "../../../../layouts/ │
│ PageLayout.astro"; │
│ import LeaderboardTable from "../../../../co │
│ mponents/Leaderboard/LeaderboardTable/Leader │
│ boardTable.astro"; │
│ import Pagination from "../../../../componen │
│ ts/Leaderboard/Pagination/Pagination.astro"; │
│ import LeaderboardScopeFilter from "../../.. │
│ /../components/Leaderboard/LeaderboardScopeF │
│ ilter/LeaderboardScopeFilter.astro"; │
│ import LeaderboardTypeNav from "../../../../ │
│ components/Leaderboard/LeaderboardTypeNav/Le │
│ aderboardTypeNav.astro"; │
│ import LeaderboardHeader from "../../../../c │
│ omponents/Leaderboard/LeaderboardHeader/Lead │
│ erboardHeader.astro"; │
│ import PlayerSearch from "../../../../compon │
│ ents/PlayerSearch/PlayerSearch.astro"; │
│ import { fetchUnifiedLeaderboard } from "../ │
│ ../../../lib/api"; │
│ import { getEffectiveRealmSlug } from "../.. │
│ /../../lib/realms"; │
│ │
│ export const prerender = false; │
│ │
│ const seasonParam = Astro.params.season || " │
│ season3"; │
│ const seasonMatch = seasonParam.match(/^seas │
│ on(\d+)$/); │
│ const isAllTime = seasonParam === "all-time" │
│ ; │
│ const currentSeason: number | "all-time" = i │
│ sAllTime │
│ ? "all-time" │
│ : seasonMatch │
│ ? parseInt(seasonMatch[1], 10) │
│ : 3; │
│ │
│ const scope = Astro.params.scope || ""; │
│ let scopeParts = scope.split("/").filter(Boo │
│ lean); │
│ │
│ // Sort lives at the front of the scope catc │
│ hall as `by-runs` (default │
│ // `by-time` is omitted). │
│ let sortParam: "time" | "runs" = "time"; │
│ if (scopeParts[0] === "by-runs") { │
│ sortParam = "runs"; │
│ scopeParts = scopeParts.slice(1); │
│ } │
│ │
│ // Old shape was /players/{sort}/... - strip │
│ and 301 to canonical. │
│ if ( │
│ scopeParts.length > 0 && │
│ (scopeParts[0] === "overall-time" || scope │
│ Parts[0] === "total-runs") │
│ ) { │
│ const tail = scopeParts.slice(1).join("/") │
│ ; │
│ return Astro.redirect( │
│ `/challenge-mode/${seasonParam}/players/ │
│ ${tail || "global"}`, │
│ 301, │
│ ); │
│ } │
│ │
│ let regionParam = "global"; │
│ let realmParam = ""; │
│ let apiScope: "global" | "regional" | "realm │
│ " = "global"; │
│ if (scopeParts.length >= 1) { │
│ regionParam = scopeParts[0]; │
│ if (regionParam !== "global") { │
│ apiScope = "regional"; │
│ if (scopeParts.length >= 2 && scopeParts │
│ [1] && scopeParts[1] !== "all") { │
│ realmParam = scopeParts[1]; │
│ apiScope = "realm"; │
│ } │
│ } │
│ } │
│ │
│ // Connected-realm rollup: only parent realm │
│ s have JSON files. If the user │
│ // landed on a child realm slug (e.g. eu/gar │
│ alon ? child of mirage-raceway), │
│ // 301 to the parent so the URL matches the │
│ data we serve. │
│ if (apiScope === "realm") { │
│ const parent = getEffectiveRealmSlug(regio │
│ nParam, realmParam); │
│ if (parent && parent !== realmParam) { │
│ const tail = scopeParts.slice(2).join("/ │
│ "); │
│ const newPath = │
│ `/challenge-mode/${seasonParam}/player │
│ s/${regionParam}/${parent}` + │
│ (tail ? `/${tail}` : ""); │
│ const qs = Astro.url.searchParams.toStri │
│ ng(); │
│ return Astro.redirect(qs ? `${newPath}?$ │
│ {qs}` : newPath, 301); │
│ } │
│ } │
│ │
│ const pageParam = Astro.url.searchParams.get │
│ ("page"); │
│ const currentPage = pageParam ? parseInt(pag │
│ eParam, 10) : 1; │
│ │
│ const seasonForNav: number = isAllTime ? 2 : │
│ (currentSeason as number); │
│ │
│ let scopeLabel: string; │
│ if (regionParam === "global") { │
│ scopeLabel = "Global"; │
│ } else if (realmParam) { │
│ scopeLabel = `${regionParam.toUpperCase()} │
│ - ${realmParam}`; │
│ } else { │
│ scopeLabel = regionParam.toUpperCase(); │
│ } │
│ const seasonLabel = isAllTime ? "All Time" : │
│ `Season ${currentSeason}`; │
│ │
│ let data; │
│ try { │
│ data = await fetchUnifiedLeaderboard( │
│ "player", │
│ sortParam, │
│ currentSeason, │
│ apiScope, │
│ { │
│ region: regionParam, │
│ realmSlug: realmParam || undefined, │
│ page: currentPage, │
│ }, │
│ Astro.url.origin, │
│ ); │
│ } catch (error) { │
│ console.error("[Players page] Error:", err │
│ or); │
│ throw error; │
│ } │
│ │
│ const pageTitle = `Player Rankings - ${scope │
│ Label} - ${seasonLabel}`; │
│ const dungeonName = │
│ sortParam === "runs" ? "Player Rankings - │
│ Total Runs" : "Player Rankings"; │
│ │
│ // Build toggle URLs - sort lives in the pat │
│ h now (`by-runs` segment between │
│ // view and scope), so toggling rebuilds the │
│ path. Other query params (e.g. │
│ // from future filters) are preserved minus │
│ `page`. │
│ const scopeTail = scopeParts.length ? scopeP │
│ arts.join("/") : "global"; │
│ const otherParams = new URLSearchParams(Astr │
│ o.url.searchParams); │
│ otherParams.delete("page"); │
│ otherParams.delete("sort"); │
│ const otherQS = otherParams.toString(); │
│ const qsSuffix = otherQS ? `?${otherQS}` : " │
│ "; │
│ │
│ function buildPath(view: "players" | "charac │
│ ters", sort: "time" | "runs") { │
│ const sortSeg = sort === "runs" ? "/by-run │
│ s" : ""; │
│ return `/challenge-mode/${seasonParam}/${v │
│ iew}${sortSeg}/${scopeTail}`; │
│ } │
│ const sortByTimeUrl = buildPath("players", " │
│ time") + qsSuffix; │
│ const sortByRunsUrl = buildPath("players", " │
│ runs") + qsSuffix; │
│ const viewPlayerUrl = buildPath("players", s │
│ ortParam) + qsSuffix; │
│ const viewCharacterUrl = buildPath("characte │
│ rs", sortParam) + qsSuffix; │
│ │
│ // Determine which LeaderboardTable type to │
│ render. │
│ const tableType = sortParam === "runs" ? "ac │
│ count-total-runs" : "account"; │
│ --- │
│ │
│ <PageLayout title={pageTitle}> │
│ <main class="leaderboard-page"> │
│ <LeaderboardHeader │
│ season={seasonForNav} │
│ dungeonName={dungeonName} │
│ scopeLabel={`${seasonLabel} - ${scopeL │
│ abel}`} │
│ /> │
│ <PlayerSearch /> │
│ <LeaderboardTypeNav │
│ currentTab="player" │
│ currentRegion={regionParam} │
│ currentRealm={realmParam} │
│ currentSeason={seasonForNav} │
│ /> │
│ │
│ <div class="cm-filters"> │
│ <div class="filter-group"> │
│ <label for="view-filter">View</label │
│ > │
│ <select id="view-filter"> │
│ <option value="player" selected da │
│ ta-url={viewPlayerUrl} │
│ >Player</option │
│ > │
│ <option value="character" data-url │
│ ={viewCharacterUrl} │
│ >Character</option │
│ > │
│ </select> │
│ </div> │
│ <div class="filter-group"> │
│ <label for="sort-filter">Sort</label │
│ > │
│ <select id="sort-filter"> │
│ <option │
│ value="time" │
│ selected={sortParam === "time"} │
│ data-url={sortByTimeUrl} │
│ > │
│ Time │
│ </option> │
│ <option │
│ value="runs" │
│ selected={sortParam === "runs"} │
│ data-url={sortByRunsUrl} │
│ > │
│ Total Runs │
│ </option> │
│ </select> │
│ </div> │
│ <LeaderboardScopeFilter │
│ currentRegion={regionParam} │
│ currentRealm={realmParam} │
│ currentSeason={isAllTime ? 0 : (curr │
│ entSeason as number)} │
│ leaderboardType="player" │
│ currentSort={sortParam} │
│ /> │
│ </div> │
│ │
│ <div id="leaderboard-content"> │
│ <LeaderboardTable │
│ type={tableType} │
│ accounts={data.leaderboard} │
│ currentPage={data.pagination.current │
│ Page} │
│ pageSize={data.pagination.pageSize} │
│ region={regionParam} │
│ realm={realmParam} │
│ /> │
│ </div> │
│ │
│ <Pagination │
│ currentPage={data.pagination.currentPa │
│ ge} │
│ totalPages={data.pagination.totalPages │
│ } │
│ hasNextPage={data.pagination.hasNextPa │
│ ge} │
│ hasPrevPage={data.pagination.hasPrevPa │
│ ge} │
│ totalPlayers={data.pagination.totalAcc │
│ ounts ?? │
│ data.pagination.totalPlayers ?? │
│ data.pagination.totalRuns ?? │
│ 0} │
│ baseUrl={Astro.url.pathname} │
│ /> │
│ </main> │
│ </PageLayout> │
│ │
│ <script> │
│ // Same data-url navigation pattern the ot │
│ her filters use. │
│ for (const id of ["view-filter", "sort-fil │
│ ter"]) { │
│ const el = document.getElementById(id) a │
│ s HTMLSelectElement | null; │
│ if (!el) continue; │
│ el.addEventListener("change", (e) => { │
│ const target = e.target as HTMLSelectE │
│ lement; │
│ const url = target.options[target.sele │
│ ctedIndex].dataset.url; │
│ if (url) window.location.href = url; │
│ }); │
│ } │
│ </script> │
│ │
│ <style> │
│ .leaderboard-page { │
│ max-width: 1200px; │
│ margin: 0 auto; │
│ padding: 20px; │
│ } │
│ │
│ @media (max-width: 768px) { │
│ .leaderboard-page { │
│ padding: 15px; │
│ } │
│ } │
│ </style> │
└──────────────────────────────────────────────┘
──────────────────────────────────────────────────────────────────────────────────────
OOKNET
────────────────────────────────────────────────
OOKNET